package core.util;

import java.util.LinkedHashMap;
import java.util.Map;

public class MapUtil {

	public static boolean isEmpty(Map<?, ?> map) {
		return (map == null || map.isEmpty());
	}

	public static boolean isNotEmpty(Map<?, ?> map) {
		return (map != null && !map.isEmpty());
	}

	public static <K, V> Map<K, V> map(Map<K, V> src, Map<K, V> desc, Map<K, K> mapping, V defValIfNull) {
		return map(src, desc, mapping, defValIfNull, false);
	}

	public static <K, V> Map<K, V> map(Map<K, V> src, Map<K, V> desc, Map<K, K> mapping, V defValIfNull,
			boolean copyUnmappedVal) {
		if (src == null || desc == null || mapping == null) {
			return desc;
		}

		for (K key : src.keySet()) {
			K mappedKey = mapping.get(key);
			if (mappedKey == null) {
				if (copyUnmappedVal) {
					mappedKey = key;
				} else {
					continue;
				}
			}

			V val = src.get(key);
			if (val == null) {
				val = defValIfNull;
			}
			desc.put(mappedKey, val);
		}

		return desc;
	}

	@SuppressWarnings("unchecked")
	public static <K, V, NK, NV> Map<NK, NV> convert(Map<K, V> map, Convertor<K, NK> keyConvertor,
			Convertor<V, NV> valConvertor) {
		if (map == null || (keyConvertor == null && valConvertor == null)) {
			return null;
		}

		Map<NK, NV> newMap = new LinkedHashMap<NK, NV>();
		for (K key : map.keySet()) {
			NK newKey;
			if (keyConvertor != null) {
				newKey = keyConvertor.convert(key);
			} else {
				newKey = (NK) key;
			}

			NV newVal;
			if (valConvertor != null) {
				newVal = valConvertor.convert(map.get(key));
			} else {
				newVal = (NV) map.get(key);
			}

			newMap.put(newKey, newVal);
		}

		return newMap;
	}

	@SuppressWarnings("unchecked")
	public static <K, V, R extends Map<K, V>> R match(R map, Matcher<K> keyMatcher, Matcher<V> valMatcher) {
		Matcher<K>[] keyMatchers = null;
		if (keyMatcher != null) {
			keyMatchers = ArrayUtil.asArray(keyMatcher);
		}
		
		Matcher<V>[] valMatchers = null;
		if (valMatcher != null) {
			valMatchers = ArrayUtil.asArray(valMatcher);
		}
		
		return matchAll(map, keyMatchers, valMatchers);
	}

	public static <K, V, R extends Map<K, V>> R match(R map, Class<? extends R> returnType, Matcher<K>[] keyMatchers,
			Matcher<V>[] valMatchers) {
		return matchAll(map, returnType, keyMatchers, valMatchers);
	}

	public static <K, V, R extends Map<K, V>> R matchAny(R map, Matcher<K>[] keyMatchers, Matcher<V>[] valMatchers) {
		return match(map, null, keyMatchers, valMatchers, false);
	}

	public static <K, V, R extends Map<K, V>> R matchAny(R map, Class<? extends R> returnType,
			Matcher<K>[] keyMatchers, Matcher<V>[] valMatchers) {
		return match(map, returnType, keyMatchers, valMatchers, false);
	}

	public static <K, V, R extends Map<K, V>> R matchAll(R collection, Matcher<K>[] keyMatchers,
			Matcher<V>[] valMatchers) {
		return match(collection, null, keyMatchers, valMatchers, true);
	}

	public static <K, V, R extends Map<K, V>> R matchAll(R map, Class<? extends R> returnType,
			Matcher<K>[] keyMatchers, Matcher<V>[] valMatchers) {
		return match(map, returnType, keyMatchers, valMatchers, true);
	}

	@SuppressWarnings("unchecked")
	private static <K, V, R extends Map<K, V>> R match(R map, Class<? extends R> returnType, Matcher<K>[] keyMatchers,
			Matcher<V>[] valMatchers, boolean isMatchAll) {
		if (isEmpty(map) || (ArrayUtil.isEmpty(keyMatchers) && ArrayUtil.isEmpty(valMatchers))) {
			return null;
		}

		R matched = (R) newMap(returnType != null ? returnType : map);
		if (matched == null) {
			return null;
		}

		for (K key : map.keySet()) {
			boolean isMatched;
			if (ArrayUtil.isNotEmpty(keyMatchers)) {
				isMatched = MatchHelper.match(key, keyMatchers, isMatchAll);
				
				if (ArrayUtil.isNotEmpty(valMatchers) && ((isMatched && isMatchAll) || (!isMatched && !isMatchAll))) {
					isMatched = MatchHelper.match(map.get(key), valMatchers, isMatchAll);
				}
			} else {
				isMatched = MatchHelper.match(map.get(key), valMatchers, isMatchAll);
			}

			if (isMatched) {
				matched.put(key, map.get(key));
			}
		}

		return matched;
	}

	@SuppressWarnings("unchecked")
	public static <M extends Map<?, ?>> M newMap(Object reference) {
		Class<Map<?, ?>> clazz = null;
		if (reference instanceof Map<?, ?>) {
			clazz = (Class<Map<?, ?>>) reference.getClass();

		} else if (reference instanceof Class<?> && Map.class.isAssignableFrom((Class<?>) reference)) {
			clazz = (Class<Map<?, ?>>) reference;
		}

		if (clazz != null) {
			try {
				return (M) clazz.newInstance();
			} catch (Exception e) {
			}
		}

		return null;
	}

	public static <K, V> Map.Entry<K, V> toEntry(K key, V value) {
		return new Entry<K, V>(key, value);
	}

	static class Entry<K, V> implements Map.Entry<K, V> {

		private K key;

		private V value;

		public Entry() {
		}

		public Entry(K key, V value) {
			this.key = key;
			this.value = value;
		}

		public K getKey() {
			return key;
		}

		/**
		 * {@inheritDoc}
		 */
		public K setKey(K key) {
			K originalKey = this.key;
			this.key = key;

			return originalKey;
		}

		/**
		 * {@inheritDoc}
		 */
		public V getValue() {
			return value;
		}

		/**
		 * {@inheritDoc}
		 */
		public V setValue(V value) {
			V originalValue = this.value;
			this.value = value;

			return originalValue;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public boolean equals(Object o) {
			if (!(o instanceof Map.Entry)) {
				return false;
			}

			Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
			Object k1 = getKey();
			Object k2 = e.getKey();
			if (k1 == k2 || (k1 != null && k1.equals(k2))) {
				Object v1 = getValue();
				Object v2 = e.getValue();
				if (v1 == v2 || (v1 != null && v1.equals(v2))) {
					return true;
				}
			}
			return false;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public int hashCode() {
			return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode());
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public String toString() {
			return getKey() + "=" + getValue();
		}
	}

	private MapUtil() {
	}
}
